<?php
if ($_SERVER['argc']<2) {
    usage();
}
echo "initializing ... ";
define('DATA_DIR','/tmp/phptags');//for tags cache mainly
define('KEYWORD_FORMAT','[a-zA-Z_][a-zA-Z_\d]*');
if (!file_exists(DATA_DIR)) {
    mkdir(DATA_DIR,0777,true)||define('CACHABLE',false);
}
define('CACHABLE',false);//A unknown bug cause it uncachable.No time to fix
if (!defined('CACHABLE')) {
    define('CACHABLE',touch(DATA_DIR.'/test'));
}
$dirs = array();
$filename_pattern  = '#.*\.(php|inc)$#i';
$tag_file = 'tags';
$recursive = false;
$verbose = true;
$var = false;
array_shift($_SERVER['argv']);
foreach ($_SERVER['argv'] as $v) {
    switch ($v) {
        case '-p':
            $var = 'p';
            continue 2;
        case '-o':
            $var = 'tag_file';
            continue 2;
        case '-v':
            $verbose = true;
            continue 2;
        case '-r':
            $recursive=true;
            continue 2;
    }
    if ($var!=false) {
        $$var = $v;
        $var  = false;
    } else {
        $dirs[]=$v;
    }
}
isset($p) && $filename_pattern='#'.$p.'$#i';
count($dirs)<1 && $dirs=array('.');
if (!file_exists($tag_file) && !touch($tag_file)) {
    exit("File(".$tag_file.") can't be created\n");
}
if (!is_writeable($tag_file)) {
    exit("File(".$tag_file.") can't be written\n");
}
$tagmtime  = filemtime($tag_file);
$selfmtime = filemtime(__FILE__);
$tags = array();
echo 'recursive..'.($recursive?'yes':'no').'  ';
echo 'cache .. '.(CACHABLE?'yes':'no').'  ';
echo "ok\n";
echo "Looping ... ";
foreach ($dirs as $dir) {
    echo $dir." .. ";
    buildTag($dir);
    echo "ok\n";
}
echo "sorting ... ";
sort($tags);
echo "ok\n";
echo "writing file ... ".count($tags).' items .. ';
file_put_contents($tag_file,implode(chr(10),$tags));
echo "ok\n";
function buildTag($dir) {
    //echo $dir."\n";
    global $filename_pattern,$tagmtime,$recursive,$k,$tags,$selfmtime;
    $ignore_file = false;
    $dir = rtrim($dir,'/');
    $d = @dir($dir);
    if ($d===false) return false;
    if (CACHABLE && $tagmtime>filemtime($dir) && $tagmtime>$selfmtime && false!==($cachetags=getCache($dir))) {
        $tags=array_merge($tags,$cachetags);
        $ignore_file = true;
    }
    $current_tags = array();
    while (false!==($e=$d->read())) {
        if ($e=='.' || $e=='..') continue;
        $f = $dir.'/'.$e;
        if (is_dir($f) && $recursive) buildTag($f);
        if ($ignore_file
        || !preg_match($filename_pattern,$e)
        || !is_file($f)) continue; 
        $fp = fopen($f,"r");
        $l=0;//line num
        $in_php = false;
        $in_comment = false;
        while (!feof($fp)) {
            $finish_comment_just_now = false;//whether a block comment ending line
            $first_words = '';//before block comment start
            $last_words = '';//after block comment finish
            $l++;
            $buffer = fgets($fp);
            if (strpos($buffer,'<?')!==false) {//simplly determine a php block
                $in_php = true;
            }
            if ($in_php===false) continue;
            if (!$in_comment && strpos($buffer,'/*')!==false) {//start comment
                $first_words = preg_replace("#/\*.*#",'',$buffer);
                $in_comment = true;
            }
            if ($in_comment && strpos($buffer,'*/')!==false) {//finish comment
                $last_words = preg_replace('#.*\*/#','',$buffer);
                $in_comment = false;
                $finish_comment_just_now = true;
            }
            if ($first_words!='' || $last_words!='') {
                $buffer = $first_words.' '.$last_words;
            } elseif ($in_comment || $finish_comment_just_now) {
                continue;
            }
            $buffer = preg_replace('#//.*#','',$buffer);
            if (false!==($tag=getTag($buffer))) {
                if (CACHABLE) {
                    $current_tags[] = $tag.chr(9).$f.chr(9).$l;
                } else {
                    $tags[] = $tag.chr(9).$f.chr(9).$l;
                }
            }
        }
        fclose($fp);
    }
    if (CACHABLE) {
        saveCache($dir,$current_tags);
        $tags=array_merge($tags,$current_tags);
    }
}
function getCache($dir) {
    if (!CACHABLE) return false;
    $data_file = DATA_DIR.'/c_'.md5($dir);
    if (file_exists($data_file) && false!==($data=file_get_contents($data_file))) {
        return unserialize($data);
    }
    return false;
}
function saveCache($dir,$tag) {
    if (!CACHABLE) return false;
    $data_file = DATA_DIR.'/c_'.md5($dir);
    return file_put_contents($data_file,serialize($tag));
}
function getTag($str) {
    /*
    ignore names with fucking format.
    */
    $tag=false;
    $ignore_tags=array(
            false,
            '__get',
            '__set',
            '__construct',
            '__destruct',
            '__call',
            '__sleep',
            '__wakeup',
            '__autoload',
            '__clone',
            '__toString',
            '__set_state'
    );
    if (preg_match("#.*function\s+(".KEYWORD_FORMAT.")\(#i",$str,$m)) {
        $tag = $m[1];
    } elseif (preg_match('#(class|const|interface)\s+('.KEYWORD_FORMAT.')#i',$str,$m)) {
        $tag = $m[2];
    } elseif (preg_match('#define\([\'"]('.KEYWORD_FORMAT.')[\'"]#i',$str,$m)) {
        $tag = $m[1];
    }
    if (in_array($tag,$ignore_tags)) return false; 
    return $tag;
}
function usage() {
    echo 'Usage: php tags.php [options] directory ...
OPTIONS
    -p $pattern  filename matching $pattern will be taken,default is #.*\.(php|inc)$#i
    -r           recursive
    -o $file     use $file to store tags data.Default is \'tags\'
    -v verbose'."\n";
    exit();

}
?>
